'''
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Created on Jul 25, 2009

Version: 

@author: bryan
'''

import JT
import os
import numpy as np
import vtk

class Volume(vtk.vtkVolume):
    """
    Setup a volume  for rendering.
    """
    def __init__(self):
        self._maskReader = vtk.vtkMetaImageReader()
        self._mask = vtk.vtkImageMask()
        
        self.cur_dir = ''
        self._volImageData = None
        self._structuredPointsReader = vtk.vtkStructuredPointsReader()
        self._metaImageReader = vtk.vtkMetaImageReader()
        self.volume_path = None
        
        # Allow the center of rotation for the volume to be set 
        self.center = [0,0,0]
        self._originalTransform = np.eye(4)
        
        # Do setup stuff, like ctf, otf
        self.SetupVolume()
        
    def SetupVolume(self):
        # The CTF setPoint will allow the brightness to be changed by adjusting this variable
        # Default values chosen from 2D3D gold standard data.
        self._ctfSetPoint = 86
        self._otfThreshold = 46
        self._otfLevel = 0.15
        self._rgb = [.0,.0,.0]

        #Default settings for the opacity transfer function,
        self._opacityTransferFunction = vtk.vtkPiecewiseFunction()
        self._opacityTransferFunction.AddPoint(0.0, 0.0)
        self._opacityTransferFunction.AddPoint(self._otfThreshold, 0.0)
        self._opacityTransferFunction.AddPoint(255, self._otfLevel)

        # Create transfer mapping scalar value to color
        self._colorTransferFunction = vtk.vtkColorTransferFunction()
        self._colorTransferFunction.AddRGBPoint(0.0, 0.0, 0.0, 0.0)
        self._colorTransferFunction.AddRGBPoint(self._ctfSetPoint, 1.0, 1.0, 1.0)
        self._colorTransferFunction.AddRGBPoint(255, 1.0, 1.0, 1.0)

        # The property describes how the data will look
        self._volumeProperty = vtk.vtkVolumeProperty()

        # Setup default properties
        self._volumeProperty.SetColor(self._colorTransferFunction)
        self._volumeProperty.SetScalarOpacity(self._opacityTransferFunction)
        self._volumeProperty.ShadeOn()
        self._volumeProperty.SetInterpolationTypeToLinear()
        self.SetProperty(self._volumeProperty)

        #This is the key part for volumer rendering TextureMapper = fast DRR
        self._volumeMapper = vtk.vtkVolumeTextureMapper3D()
        self.SetMapper(self._volumeMapper)
    
    def UseRayCastMapper(self):
        """
        This will result in slower, but [potentially] more accurate rendering.
        """
        rcFunction = vtk.vtkVolumeRayCastCompositeFunction()
        self._volumeMapper = vtk.vtkVolumeRayCastMapper()
        self._volumeMapper.SetVolumeRayCastFunction(rcFunction)
        self.SetMapper(self._volumeMapper)
        if self._volImageData == None:
            print 'Image data not set'
            return
        self._volumeMapper.SetInput(self._volImageData)
        
    
    def UseTextureMapper(self):
        """
        This will result in faster, but [potentially] less accurate rendering.
        """
        self._volumeMapper = vtk.vtkVolumeTextureMapper3D()
        self.SetMapper(self._volumeMapper)
        self._volumeMapper.SetInput(self._volImageData)

    def SetInput(self,vtkImageData):
        """
        Set the input image data for the mapper.
        """
        self._volumeMapper.SetInput(vtkImageData)

    def SetImageMask(self,maskPath):
        """
        Set the image mask.
        """
        if maskPath != None:
            self._maskReader.SetFileName(maskPath)
            self._maskReader.Update()
            self._mask.SetMaskInput(self._maskReader.GetOutput())
            self._mask.SetImageInput(self._volImageData)
            self._volumeMapper.SetInput(self._mask.GetOutput())
        else:
            self._volumeMapper.SetInput(self._volImageData)


    def SetVolumeFileName(self,volume_path):
        """
        Specify the path to a 3D volume and add to mapper.
        """
        # Select the correct reader based on the filename extension
        ext = volume_path[volume_path.rfind('.')+1:].lower()
        # logging.debug('Volume to be opened: %s' % volume_path)
        if (ext == 'mha'or ext == 'mhd'):
            self._reader = self._metaImageReader
        elif ext == 'vtk':
            self._reader = self._structuredPointsReader
        elif ext == 'stl':
            print "Try using the SurfaceModel class instead of the Volume class"
            return
        else:
            print "No reader has been implemented for %s type files" % ext
            return

        self._reader.SetFileName(volume_path)
        self._volImageData = self._reader.GetOutput()
        
        #Save some data about the image
        self._volImageData.Update()
        self.pixel_type = self._volImageData.GetScalarTypeAsString()
        self.pixel_range = self._volImageData.GetScalarRange()
        self.dimensions = self._volImageData.GetDimensions()
        self.spacing = self._volImageData.GetSpacing()
        
        # Set the image as the input to the shrinker, which is connected to the 
        # volume mapper:
        self.SetInput(self._reader.GetOutput())

        # if the user previous set custom display settings for this volume, use them
        # TODO: this is going to be a problem when the new data class is implemented
        #self.OpenDisplaySettings(volume_path)
        self.volume_path = volume_path
    
    def AddBodyCenter(self,center):
        """
        Deprecated! Use SetBodyCenter instead.
        """
        print 'Deprecated! Use SetCenter instead.'
        self.SetCenter(center)
    
    def SetCenter(self,center):
        """
        Set the rotational center of the object to register.
        """
        self.center = center
        #self.SetOrigin(center)
        
    
    def GetCenterRotation(self):
        """
        Get the rotational center of the object to register.
        """
        print "Volume.GetCenterRotation"
        body_center = np.asarray(self.center)
        volume_center = np.asarray(self.GetCenter())
        center_rotation = body_center+volume_center
        
        return center_rotation
    
    def GetBodyCenter(self):
        return self.bodyCenters[self._activeBody]
    
             
   
    def SetPose(self,pose):
        """
        Transform the volume according to the pose = Tx, Ty, Tz, Rx, Ry, Rz 
        Translations are in mm and rotations are in radians.  
        
        ***********************WARNING*****************************************
        This is really just an offset from the originalTransform, not 
        the pose in the World Coordinate System!!!
        
        Use SetWorldPose to set the pose using pose relative to World Coord
        System (what you get from GetPose).  This needs to be fixed!
        ***********************************************************************
        
        this implementation is consistent with startpositions_200_ordered.txt
        
        This function will:
            1. shift the volume -center (so that rotation is about the center)
            2. rotate/translate the volume according to pose
            3. shift back by +center
            
        See Data_Help.txt for explanation of transformation process. 
        """
        # Top is TOriginalPositionWCS, the original position of the volume 
        # this should is specified during calibration (from sta file)
        # To set the start position, the volume will first be reset to the original
        # transform 
        self.SetTransform(self._originalTransform)
        transform = self.GetTransformFromPose(pose)
        startTransform = self.DoTransform(transform)
        self.SetTransform(startTransform)
        self._startTransform = startTransform
    
    
    def SetOriginalTransform(self,transform):
        """
        Specify the original transform of the volume (4x4 numpy array).
        Only call this ONCE with starting postition of the volume.
        """
        #print 'Volume original transform has been set to %s' % transform
        self._originalTransform = transform
        # Set to User Matrix
        self.SetTransform(transform)
        
        
    def SetTransform(self,transform):
        """
        Specify the current transform of the volume (4x4 numpy array)
        this function will convert to a VTK matrix and update the current 
        user matrix of the volume. 
        The transform should be about the volumes center of rotation (not origin)
        """
        vtkMatrix = JT.utils.GetVTKMatrixFromNumpyArray(transform)
        self.SetUserMatrix(vtkMatrix)
        
    
    def DoTransform(self,transform):
        """
        Apply Transform after shifting the volume to the center of rotation. 
        This transform is concatenated to the current transform of the volume.
        This function will return a 4x4 numpy array.
        
        If SetStartPosition is called, it will reset the current pose to be 
        the original position of the volume before calling this function.
        """
        # Top is the original transform of the volume.
        Top = self._originalTransform
        # Tco is TTranscenterInv ( vertebral body center to volume origin)
        Tco = np.eye(4)
        Tco[0:3,3] = -np.asarray(self.center,'f')
        # Toc is TTranscenter (volume origin to vertebral body center)
        Toc = np.eye(4)
        Toc[0:3,3] = np.asarray(self.center,'f')
        # Tnp is new pose of the volume in WCS
        Tnp = np.dot(Toc,np.dot(transform,np.dot(Tco,Top)))
        return Tnp
       
  
    def GetResidualPose(self,transform1,transform2):
        """
        Return the pose that will transform the transform1->transform2.
        """
        # Top is TOriginalPositionWCS, the original position of the volume 
        # this should is specified during calibration (from sta file)
        Top = self._originalTransform
        # Tco is TTranscenterInv ( vertebral body center to volume origin)
        Tco = np.eye(4)
        Tco[0:3,3] = -np.asarray(self.center,'f')
        #T2shifted is transfrom2 shifted to center of body
        T2shifted = np.dot(Tco,transform2)
        #T1shifted is transform1 shifted to center of body)
        T1shifted = np.dot(Tco, transform1)
        #T1shiftedInverse is TOriginalErrorPositionInv (Original error postition invoerse)
        T1shiftedInverse = np.linalg.inv(T1shifted)
        # Tdiff is residual pose; the pose that will transform the original volume to the given transform
        Tdiff = np.dot(T2shifted,T1shiftedInverse)
        pose = self.GetPoseFromTransform(Tdiff)
        return pose
    
    def GetTransformFromPose(self,pose):
        """
        Given an arbitrary pose (6 element vector), return a transform (4x4 numpy array)
        derived from T=Rz*Ry*Rx.
        """
        Tx,Ty,Tz,Rx,Ry,Rz = pose
        
        # transform is TStartTransformation (body center to start pose)
        transform = np.eye(4)
        transform[0,0] = np.cos(Ry)*np.cos(Rz)
        transform[0,1] = -np.cos(Rx)*np.sin(Rz) + np.cos(Rz)*np.sin(Rx)*np.sin(Ry)
        transform[0,2] = np.sin(Rx)*np.sin(Rz) + np.cos(Rx)*np.cos(Rz)*np.sin(Ry)
        transform[0,3] = Tx
        transform[1,0] = np.cos(Ry)*np.sin(Rz)
        transform[1,1] = np.cos(Rx)*np.cos(Rz) + np.sin(Rx)*np.sin(Ry)*np.sin(Rz)
        transform[1,2] = -np.cos(Rz)*np.sin(Rx) + np.cos(Rx)*np.sin(Ry)*np.sin(Rz)
        transform[1,3] = Ty
        transform[2,0] = -np.sin(Ry)
        transform[2,1] = np.cos(Ry)*np.sin(Rx)
        transform[2,2] = np.cos(Rx)*np.cos(Ry)
        transform[2,3] = Tz
        
        return transform
        
    
    def GetPoseFromTransform(self,transform):
        """
        Given a 4x4 transform in numpy, return a 6 element vector 
        (Tx,Ty,Tz,Rx,Ry,Rz).  Rotation order: T=Rz*Ry*Rx [x->y->z]
        """
        Tx = transform[0][3]
        Ty = transform[1][3]
        Tz = transform[2][3]
        Ry = -np.arcsin(transform[2][0])
        Rx = np.arcsin(transform[2][1]*1/np.cos(Ry))
        Rz = np.arcsin(transform[1][0]*1/np.cos(Ry)) 
        
        pose=np.array([Tx,Ty,Tz,Rx,Ry,Rz])
        
        return pose
    
    def GetPose(self): 
        """
        Return the current 6 element vector (Tx,Ty,Tz,Rx,Ry,Rz)
        That describes the position and orientation of the volume.
        """
        # transform is the current transformation of the volume
        transform = JT.utils.GetNumpyArrayFromVTKMatrix(self.GetUserMatrix())
        pose = self.GetPoseFromTransform(transform)
        return pose     
    
    def GetPoseFromOriginalToCurrent(self): 
        """
        Return the current 6 element vector (Tx,Ty,Tz,Rx,Ry,Rz)
        That describes the postition and orientation of the volume
        with respect to the original pose of the volume.
        """
        # transform is the current transformation of the volume
        transform = JT.utils.GetNumpyArrayFromVTKMatrix(self.GetUserMatrix())
        pose = self.GetResidualPose(self._originalTransform,transform)
        return pose 


    def OpenDisplaySettings(self,volume_path):
        """
        Given the path to the volume that is being opened for rendering, find
        (if it exists) a display settings file and read the settings in to be
        used for volume rendering.
        """
        # Try to open volume dispay settings (a file with same name as volume,
        # with a .disp extension)
        (basePath,volName) = os.path.split(volume_path)
        shortVolName = volName[:volName.rfind('.')]
        dispSettingsName = os.path.join(basePath,shortVolName+'.disp')
        self._dispSettingsName = dispSettingsName

        try:
            dispFile = open(dispSettingsName)
            line1=dispFile.readline().split()
            line2=dispFile.readline().split()

            # Make sure the file contains the expected info
            if line1[0] == "opacityLevel:":
                opacityLevel = float(line1[1])
            else:
                print "Error: %s is not the expected variable name" % line1[0]

            if line2[0] == "opacityThreshold:":
                opacityThreshold = float(line2[1])
            else:
                print "Error: %s is not the expected variable name" % line1[0]

        except:
            print "Error opening display settings %s " % dispSettingsName
            opacityLevel = 0.15
            opacityThreshold = 40.0
            print "Using Defaults (%f,%f)" % (opacityLevel,opacityThreshold)
            # logging.debug("Could not find display settings file, one will be created if the default values are changed")

        self.SetOpacityLevel(opacityLevel)
        self.SetOpacityThreshold(opacityThreshold)
        #self.UpdateVolumeProperties()

    def SetColor(self,rgb=[0,0,0]):
        """
        Change the color of the rendered volume so that it can be blended
        together with the fixed image for better visualization

        Default is white on black [1,1,1]
        """
        self._rgb = rgb
        self._colorTransferFunction.AddRGBPoint(0.0, 0.0, 0.0, 0.0)
        self._colorTransferFunction.AddRGBPoint(self._ctfSetPoint, rgb[0], rgb[1], rgb[2])
        self._colorTransferFunction.AddRGBPoint(self.pixel_range[1], rgb[0], rgb[1], rgb[2])
        #self.UpdateVolumeProperties()

    def SetBrightness(self,ctfSetPoint):
        """
        A method to allow the brightness of the color transfer function to be set.
        The brightness is set by specifying the midpoint on the ramp function,
        the low point is fixed at 0 and the high point is fixed at self.pixel_range[1].

        setPoint should be a scalar between 0 and self.pixel_range[1].
        """
        # First Remove the existing point
        index = self._colorTransferFunction.RemovePoint(self._ctfSetPoint)
        if index == -1:
            print "Error setting color point."
            return

        # Save the new point for use later
        self._ctfSetPoint = ctfSetPoint
        self._colorTransferFunction.AddRGBPoint(self._ctfSetPoint, self._rgb[0], self._rgb[1], self._rgb[2])

    def SetOpacityThreshold(self,otfThreshold):
        """
        method to allow the transparency of the opacity transfer function to be set.
        The Opacity is set by specifying the threshold on the ramp function,
        the low point is fixed at 0 and the high point is fixed at self.pixel_range[1].

        setThreshold should be an integer between 0 and self.pixel_range[1].
        """
        if (otfThreshold < 0) or (otfThreshold > self.pixel_range[1]):
            print "Error setting threshold value to %4.1f, scalar range(%4.1f,%4.1f)" %\
                   (otfThreshold, self.pixel_range[0],self.pixel_range[1])
            return

        # First Remove the existing point
        self._opacityTransferFunction.RemoveAllPoints()
        
        self._opacityTransferFunction.AddPoint(0.0, 0.0)
        self._opacityTransferFunction.AddPoint(otfThreshold, 0.0)
        self._opacityTransferFunction.AddPoint(self.pixel_range[1], self._otfLevel)
        
        # Save the new point for use later
        self._otfThreshold = otfThreshold


    def SetOpacityLevel(self,otfLevel):
        """
        method to allow the transparency of the opacity transfer function to be set.
        This function changes the maximum opacity of the volume.

        otfValue should be a float between 0 and 1.
        """
        # Check to make sure the values is between 0 and 1
        if (otfLevel < 0) or (otfLevel > 1):
            print "Error setting opacity value."
            return

        # Save the new point for use later
        self._otfLevel = otfLevel
        # The point is currently fixed at self.pixel_range[1],should this be adjustable
        self._opacityTransferFunction.AddPoint(self.pixel_range[1], self._otfLevel)
    
    def ProcessEvents(self,obj,event):
        """
        Event handler for this class.  This is a work in progress.
        """
        pass
        

class Volume2(Volume):
    """
    The original Volume class assumes that the original position of the 
    volume has a special significance (designed for the gold_standard data,
    the initial position was the optimum pose) and supplied poses were 
    applied to offset the volume from the original position.  In more general
    cases, this will not be the case, we will not want the transforms to 
    always be relative to the initial guess, we would like them to be 
    relative to the world coordinate system.
    
    This class changes the transform calculations so that the pose 
    is always relative to the world coordinate system.  Specifcally, the 
    function DoTransform was changed so the the center was appended to the 
    supplied pose, but not concatenated with the original position.
    """        
    def __init__(self):
        Volume.__init__(self)
    
    def SetPose(self,pose):
        """
        Transform the volume according to the pose = Tx, Ty, Tz, Rx, Ry, Rz 
        Translations are in mm and rotations are in radians.  
        """
        pose = np.asarray(pose)
        translation = pose[0:3]
        # Convert rotations from radians to degrees
        rotation = pose[3:6]*180/np.pi
        # Translate and rotate about world axes
        self.SetOrientation(rotation)
        self.SetPosition(translation)
        
    
    def GetPose(self): 
        """
        Return the current 6 element vector (Tx,Ty,Tz,Rx,Ry,Rz)
        That describes the position and orientation of the volume.
        rotations are in radians!
        """
        # transform is the current transformation of the volume
        position = np.asarray(self.GetPosition())
        orientation = self.GetOrientation()
        pose = [position[0],
                position[1],
                position[2],
                orientation[0]*np.pi/180,
                orientation[1]*np.pi/180,
                orientation[2]*np.pi/180]
        return pose 

class Volume3(Volume):
    """
    This class uses a custom Transformation class (derived from vtk.vtkTransform)
    It was created for use in the latest version of JointTrack image registration.
    """        
    def __init__(self):
        Volume.__init__(self)
        self.transform = JT.Transform()
        self.SetUserTransform(self.transform)
        
    def SetOriginalTransform(self,transform):
        """
        Specify the original transform of the volume (4x4 numpy array).
        Only call this ONCE with starting postition of the volume.
        """
        #print 'Volume original transform has been set to %s' % transform
        self._originalTransform = transform
        # Set to User Matrix
        self.transform.set_volume_transform(transform)
    
    def SetPose(self,pose):
        """
        Transform the volume according to the pose = Tx, Ty, Tz, Rx, Ry, Rz 
        Translations are in mm and rotations are in radians.  
        """
        pose = np.asarray(pose)
        self.transform.set_pose(pose)
        
    
    def GetPose(self): 
        """
        Return the current 6 element vector (Tx,Ty,Tz,Rx,Ry,Rz)
        That describes the position and orientation of the volume.
        rotations are in radians!
        """
        pose = self.transform.get_pose()
        return pose 
    
    def SetCenter(self,center):
        """
        Set the rotational center of the object to register.
        """
        self.transform.set_body_center(center)
        #self.SetOrigin(center)

class SurfaceModel(vtk.vtkActor):
    """
    Create an actor that can be used an isosurface.
    """
    def __init__(self):
        self._mapper=vtk.vtkPolyDataMapper()
        self._maskReader = vtk.vtkMetaImageReader()
        self._mask = vtk.vtkImageMask()
        self._surfaceModel = None
        self._maskPath = None
        self.volume_path = None
        self._otfLevel = 1
        self._otfThreshold = 1 # Not used    
        
    def SetInput(self,data):
        """
        Set the input volume image data for the marching cubes filter
        """
        self._volImageData = data
        #self._mapper.SetInput(data)
        
    def SetVolumeFileName(self,volume_path):
        """
        Specify the path to a 3D volume and add to mapper.
        """
        # Select the correct reader based on the filename extension
        ext = volume_path[volume_path.rfind('.')+1:].lower()
        # logging.debug('Volume to be opened: %s' % volume_path)
        if (ext == 'mha'or ext == 'mhd'):
            reader = vtk.vtkMetaImageReader()
        elif ext == 'vtk':
            reader = vtk.vtkStructuredPointsReader()
        elif ext == 'stl':
            self.LoadSTL(volume_path)
            self.volume_path = volume_path
            return
        else:
            print "SurfaceModel: No reader has been implemented for %s type files" % ext
            return

        reader.SetFileName(volume_path)
        #self.SetInput(reader.GetOutput())
        self._volImageData = reader.GetOutput()
        self._volImageData.Update()
        
        #Save some data about the image
        self.pixel_type = self._volImageData.GetScalarTypeAsString()
        self.pixel_range = self._volImageData.GetScalarRange()
    
    def LoadSTL(self,filename):
        reader = vtk.vtkSTLReader()
        reader.SetFileName(filename)
        reader.Update()
        self._surfaceModel = reader.GetOutput()
        #print reader.GetOutput()
        self._mapper.SetInput(reader.GetOutput())
        props = self.GetProperty()
        props.SetColor(0,0,1)
        self.SetMapper(self._mapper)
        

    def CreateIsoSurface(self,value=80,reduction=0.5):
        """
        Create an iso surface.
        By default the threshold is set to 80 and the 
        number of triangles will be reduced by 50%.
        """    
        marching=vtk.vtkImageMarchingCubes()
        marching.SetInput(self._volImageData)
        marching.SetValue(0,value)
        
        prop=self.GetProperty()
        prop.SetColor(0.0/255.0  , 33.0/255.0, 165.0/255.0) # blue
        #prop.SetColor(255.0/255.0, 74.0/255.0,   0.0/255.0) # orange
        reducer = vtk.vtkQuadricDecimation()
        reducer.SetInput(marching.GetOutput())
        reducer.SetTargetReduction(reduction)
        
        self._mapper.SetInput(reducer.GetOutput())
        self.SetMapper(self._mapper)
        
        self._surfaceModel = reducer.GetOutput()
        #print self
    
    def SetColor(self,rgb=[.2,.2,.2]):
        prop=self.GetProperty()
        prop.SetColor(rgb)
    
    def SetOpacityLevel(self,level):
        """
        Set the opacity of the actor
        """
        prop = self.GetProperty()
        prop.SetOpacity(level)
        self._otfLevel = level
    
    def SaveAsSTL(self,filename):
        """
        """
        if self._surfaceModel != None:
            writer = vtk.vtkSTLWriter()
            writer.SetInput(self._surfaceModel)
            writer.SetFileName(filename)
            writer.Write()
    
    def SetPose(self,pose):
        """
        Transform the volume according to the pose = Tx, Ty, Tz, Rx, Ry, Rz 
        Translations are in mm and rotations are in radians.  
        """
        pose = np.asarray(pose)
        translation = pose[0:3]
        # Convert rotations from radians to degrees
        rotation = pose[3:6]*180/np.pi
        # Translate and rotate about world axes
        self.SetOrientation(rotation)
        self.SetPosition(translation)
        
    
    def GetPose(self): 
        """
        Return the current 6 element vector (Tx,Ty,Tz,Rx,Ry,Rz)
        That describes the position and orientation of the volume.
        rotations are in radians!
        """
        # transform is the current transformation of the volume
        position = np.asarray(self.GetPosition())
        orientation = self.GetOrientation()
        pose = [position[0],
                position[1],
                position[2],
                orientation[0]*np.pi/180,
                orientation[1]*np.pi/180,
                orientation[2]*np.pi/180]
        return pose 
    
    def SetOriginalTransform(self,transform):
        """
        Specify the original transform of the volume (4x4 numpy array).
        Only call this ONCE with starting postition of the volume.
        """
        #print 'Volume original transform has been set to %s' % transform
        self._originalTransform = transform
        # Set to User Matrix
        self.SetTransform(transform)
        
    def SetTransform(self,transform):
        """
        Specify the current transform of the volume (4x4 numpy array)
        this function will convert to a VTK matrix and update the current 
        user matrix of the volume. 
        The transform should be about the volumes center of rotation (not origin)
        """
        vtkMatrix = JT.utils.GetVTKMatrixFromNumpyArray(transform)
        self.SetUserMatrix(vtkMatrix)
